package com.songyang.yougnsql.core.transaction;

import com.songyang.yougnsql.common.ErrorMsg;
import com.songyang.yougnsql.common.ExceptionHandler;
import com.songyang.yougnsql.common.YoungSQLException;
import com.songyang.yougnsql.core.utils.ByteUtils;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName TransactionManagerImpl
 * @Description
 * @date 2022/12/2 14:06
 * @Author yanceysong
 * @Version 1.0
 */
public class TransactionManagerImpl implements TransactionManager {

    // XID文件头长度
    protected static final int LEN_XID_HEADER_LENGTH = 8;
    // XID 文件后缀
    static final String XID_SUFFIX = ".xid";
    // 每个事务的占用长度
    private static final int XID_FIELD_SIZE = 1;
    // 事务的三种状态
    private static final byte FIELD_TRAN_ACTIVE = 0;
    private static final byte FIELD_TRAN_COMMITTED = 1;
    private static final byte FIELD_TRAN_ABORTED = 2;
    private final RandomAccessFile file;
    private final FileChannel fileChannel;
    private final Lock counterLock;
    private long xidCounter;

    TransactionManagerImpl(RandomAccessFile randomAccessFile, FileChannel fileChannel) {
        this.file = randomAccessFile;
        this.fileChannel = fileChannel;
        counterLock = new ReentrantLock();
        checkXIDFile();
    }

    /**
     * 检查xid文件是否合法
     * 规则：
     * 1、读取文件头记录的id数量
     * 2、读取文件的真实字节长度
     * 3、比较文件头记录的与真实获取的长度是否相等，不相等则文件被修改过
     */
    private void checkXIDFile() {
        try {
            //获取的真实文件长度
            long realFileLength = file.length();
            //如果获取的长度比文件头还要小，则说明该文件被修改过
            if (realFileLength < LEN_XID_HEADER_LENGTH) {
                throw new YoungSQLException(ErrorMsg.BADXID_FILE_EXCEPTION);
            }
            //如果没有问题，就获取文件头记录的长度最比较
            ByteBuffer buf = ByteBuffer.allocate(LEN_XID_HEADER_LENGTH);
            //把指针的位置指向开头
            fileChannel.position(0);
            //开始读取文件头的数据
            fileChannel.read(buf);
            //管理的格式就是最后一个Xid
            this.xidCounter = ByteUtils.parseLong(buf.array());
            if (getXidPosition(this.xidCounter + 1) != realFileLength) {
                throw new YoungSQLException(ErrorMsg.BADXID_FILE_EXCEPTION);
            }
        } catch (NullPointerException | YoungSQLException | IOException e) {
            ExceptionHandler.handler(e);
        }
    }

    /**
     * 根据xid获取该id在文件中的偏移位置
     *
     * @param xid xid
     * @return 在文件中的偏移位置
     */
    private long getXidPosition(long xid) {
        return LEN_XID_HEADER_LENGTH + (xid - 1) * XID_FIELD_SIZE;
    }

    /**
     * 根据xid更新在文件当中的最新状态
     *
     * @param xid       要更新状态的id
     * @param newStatus 最新的状态
     */
    private void updateXID(long xid, byte newStatus) {
        //获取这个xid在文件中的位置
        long xidPosition = getXidPosition(xid);
        byte[] newStatusBytes = new byte[XID_FIELD_SIZE];
        newStatusBytes[0] = newStatus;
        ByteBuffer buf = ByteBuffer.wrap(newStatusBytes);
        try {
            //指向这个id的位置
            fileChannel.position(xidPosition);
            //写进去这个值
            fileChannel.write(buf);
            //将Channel里面还未写入的数据全部刷新到磁盘
            fileChannel.force(false);
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
    }

    /**
     * 在文件头中增加一个xid
     */
    private void incrXIDInHeader() {
        //最新的xid计数器自加
        this.xidCounter++;
        //打包成数组再打包成buffer
        ByteBuffer buf = ByteBuffer.wrap(ByteUtils.long2Byte(xidCounter));
        try {
            //写进文件头
            fileChannel.position(0);
            fileChannel.write(buf);
            fileChannel.force(false);
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
    }

    /**
     * 获取一个事务的状态
     *
     * @param xid xid
     * @return 他的状态
     */
    private byte getXIDStatus(long xid) {
        long xidPosition = getXidPosition(xid);
        ByteBuffer buf = ByteBuffer.wrap(new byte[XID_FIELD_SIZE]);
        try {
            fileChannel.position(xidPosition);
            fileChannel.read(buf);
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
        return buf.array()[0];
    }

    /**
     * 检查一个事务是否处于某个状态
     *
     * @param xid    事务的id
     * @param status 要检查的状态
     * @return 检查结果
     */
    private Boolean checkXIDStatus(long xid, byte status) {
        return getXIDStatus(xid) == status;
    }

    /**
     * 开启一个事务，返回事务id
     *
     * @return 事物的id
     */
    @Override
    public long begin() {
        counterLock.lock();
        try {
            long xid = xidCounter + 1;
            //更新这个xid位置的状态
            updateXID(xid, FIELD_TRAN_ACTIVE);
            //把最新的xid数量写到文件头
            incrXIDInHeader();
            return xid;
        } finally {
            counterLock.unlock();
        }
    }

    /**
     * 提交一个事务
     *
     * @param xid 事务的id
     */
    @Override
    public void commit(long xid) {
        updateXID(xid, FIELD_TRAN_COMMITTED);
    }

    /**
     * 回滚一个事务
     *
     * @param xid 事务的id
     */
    @Override
    public void abort(long xid) {
        updateXID(xid, FIELD_TRAN_ABORTED);
    }

    /**
     * 检测一个事务是否处于Active状态
     *
     * @param xid 事务的id
     * @return 是否处于Active状态
     */
    @Override
    public boolean isActive(long xid) {
        //这里要考虑super_xid
        return xid != SUPER_XID && checkXIDStatus(xid, FIELD_TRAN_ACTIVE);
    }

    /**
     * 判断一个事务是否已经提交
     *
     * @param xid 事务的id
     * @return 是否已经提交
     */
    @Override
    public boolean isCommitted(long xid) {
        //这里要考虑super_xid
        return xid == SUPER_XID || checkXIDStatus(xid, FIELD_TRAN_COMMITTED);
    }

    /**
     * 判断一个事务是否已经回滚
     *
     * @param xid 事务的id
     * @return 是否已经回滚
     */
    @Override
    public boolean isAborted(long xid) {
        return xid != SUPER_XID && checkXIDStatus(xid, FIELD_TRAN_ABORTED);
    }

    @Override
    public void close() {
        try {
            fileChannel.close();
            file.close();
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
    }
}
